Repository navigation
Fix: deliver backend events to the chat on desktop (spinner never stops, #462) - #488
Merged
Merged
Conversation
Components loaded Tauri's listen() with require() behind a window.__TAURI__ check. Neither works: require() does not exist in Vite's ESM bundle, and window.__TAURI__ is only defined when withGlobalTauri is enabled (it is not), while web mode installs a fake one, so the check is false on desktop and true in the browser (winfunc#462). Add src/lib/tauriEvents.ts: a static import of Tauri's listen, chosen at call time with the same environment detection the API adapter uses for commands (exported as isTauriRuntime), and a DOM CustomEvent fallback for web mode. The selection logic lives in a dependency-free core module so it can be checked without a Tauri runtime.
ClaudeCodeSession always ended up with its DOM-event fallback in the desktop app (see previous commit), so claude-output / claude-complete events from the backend never arrived: the chat showed a spinner until it was reopened and cancel did nothing because the session id from the init message was never received (winfunc#462). Use the shared listen() from @/lib/tauriEvents. The listener call sites are unchanged.
FloatingPromptInput loaded getCurrentWebviewWindow with the same require()/window.__TAURI__ pattern, so on desktop it always got a stub without onDragDropEvent and dropping images into the prompt did nothing (same root cause as winfunc#462). Import it statically and register the drag-drop listener only when running in the Tauri runtime.
Replace the last require('@tauri-apps/api/event') with a static ESM import so
no component depends on require() at runtime. The hook keeps its existing
desktop/web branches; it is currently only used by
ClaudeCodeSession.refactored.tsx.
Native drag-drop events are window-wide and every chat tab stays mounted (inactive tabs are only hidden), so now that the drag-drop listener is registered on desktop, dropping an image added its path to the prompt of every open chat tab. Ignore drag-drop events in tabs whose prompt is not visible.
The backend emits each line on the session-scoped channel and then on the generic one. The chat starts with generic listeners and swaps in scoped ones after the init message; during the swap both are active, so lines arriving then were processed twice. This path was unreachable on desktop before events were delivered (winfunc#462). Once a scoped listener has received an event on a channel, the generic listener for that channel ignores it. Flags are per channel so a completion event is never skipped.
Move the "is this prompt on screen" check used by the drag-drop handler into src/lib/domVisibility.ts so it can be tested without rendering the component. No behaviour change.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
On the desktop app, after sending a prompt the chat shows a loading spinner forever and no response appears, although Claude runs and answers. The answer only shows up after closing and reopening the chat, because history is then read from the session file. Cancel does nothing, because the session id from the
initmessage never arrives. Dragging an image into the prompt does nothing either.Two defects combined:
if (window.__TAURI__). In Tauri v2,window.__TAURI__only exists whenapp.withGlobalTauriis enabled, which it is not intauri.conf.json, so on desktop the check is false. It is also inverted: web mode'sinitializeWebMode()installs a fakewindow.__TAURI__, so the check is true in the browser.require()in an ESM bundle. Inside the check the code calledrequire("@tauri-apps/api/event").requiredoes not exist in Vite's ESM output, so even withwithGlobalTaurienabled it throws, and thecatchhides it ("Tauri APIs not available, using web mode").Either way, the desktop app listened for DOM events on
windowwhile the backend emits through Tauri IPC (claude-output,claude-error,claude-complete), so nothing arrived.FloatingPromptInput.tsxused the same pattern forgetCurrentWebviewWindow(drag-and-drop got a stub withoutonDragDropEvent), anduseClaudeMessages.tshad the samerequire().Approach
src/lib/tauriEvents.ts): statically imports Tauri'slistenand decides at call time whether to use it, with the detection the API adapter already uses for commands (detectEnvironment, which checks__TAURI_INTERNALS__; exported asisTauriRuntime). Commands and events can therefore never disagree about the environment. Web mode keeps the DOMCustomEventfallback with the same{ payload }shape.src/lib/tauriEventsCore.ts): the selection logic (createListen) has no runtime imports, so it can be checked without a Tauri runtime.ClaudeCodeSession.tsx): therequire()block and inline fallback are replaced by the sharedlisten; listener call sites are unchanged.FloatingPromptInput.tsx): staticgetCurrentWebviewWindowimport; the native listener is registered only in the Tauri runtime.useClaudeMessages.ts: static import instead ofrequire(), behaviour otherwise unchanged (currently only used byClaudeCodeSession.refactored.tsx).src/lib/domVisibility.ts).init. Lines arriving during the swap were handled twice. Once a scoped listener receives an event on a channel, the generic listener for that channel ignores it. Flags are per channel, so a completion event is never skipped.withGlobalTauristays off (it would not fix therequire()layer and would expose the whole Tauri API onwindow). No Rust changes, no new dependencies.Testing
Reproduce-then-fix check. The original code from
main(TypeScript types stripped with Node'sstripTypeScriptTypes, logic untouched) and this branch were run in the same simulated environments. Desktop means__TAURI_INTERNALS__present, nowindow.__TAURI__and norequire, as in the real app. Web mode means afterinitializeWebMode(). A fake IPC emitter stands in for the backend'sapp.emit().mainclaude-outputandclaude-completereach the chatWith
withGlobalTauriforced on, the original code still falls back to DOM events, logging "Tauri APIs not available, using web mode". That confirms therequire()layer.Listener switch. A model of the chat's generic-to-scoped switch, with the backend's emit order (scoped, then generic), slow listener registration, and 0–20 lines arriving mid-switch:
Unit checks
listenrejects instead of silently falling back.Build and static checks
npx tsc --noEmit: no new errors. The only error is the existing one insrc/lib/analytics/index.ts.vite buildandtauri build --debug --bundles appsucceed.cargo test: 9/9 pass (no Rust changes).require()of Tauri APIs remains insrc/.Manual (macOS desktop build)
Documentation
New exports have JSDoc comments. No README or configuration change; no new dependencies.
Fixes #462